Table of Contents

  • 1  Предобработка данных
  • 2  Исследовательский анализ данных
    • 2.1  Общее количество покупок
    • 2.2  Общее количество уникальных пользователей
    • 2.3  Определение оптимального периода для анализа
    • 2.4  Общая выручка магазина
    • 2.5  Средний чек покупателя
    • 2.6  Среднее количество продуктов в заказе
    • 2.7  Самые популярные товары интернет-магазина
    • 2.8  Самые прибыльные товары для компании
    • 2.9  Средняя частота покупок
    • 2.10  Общий вывод по разделу
  • 3  Сегментация пользователей
    • 3.1  RFM-анализ
    • 3.2  Определение товарных категорий
    • 3.3  Общий вывод
  • 4  Проверка статистических гипотез
    • 4.1  Группировка данных для проведения анализа
    • 4.2  Формулировка нулевой и альтернативной гипотез
    • 4.3  Проведение теста и интерпретация результатов.
  • 5  Результат исследования

Выпускной проект "E-commerce — Выявление профилей потребления"¶

Презентация

Дашборд

Заказчик: Интернет-магазин товаров для дома «Пока все ещё тут»

Цель исследования:

  • Сегментировать покупателей по профилю потребления и сформулировать рекомендации для бизнеса для дальнейшей работы с сегментами.

План работы:

  1. Подготовить предоставленные данные к корректному анализу, провести предобработку данных - проверить на пропуски, явные/неявные дубликаты, корректный тип данных и т.д.
  2. Провести исследовательский анализ данных:
    • определить общее количество покупок в датасете;
    • найти общее количество уникальных пользователей в выгрузке;
    • найти максимальную и минимальную даты, зарегистрированные в датасете;
    • определить оптимальный период для дальнейшего анализа;
    • определить общую выручку магазина, средний чек с покупателя;
    • найти среднее количество продуктов в заказе;
    • посмотреть динамику продаж;
    • найти самые прибыльные товары для магазина (провести ABC-анализ);
    • определить среднюю частоту покупок (как часто пользователи возвращаются за повторной покупкой);
    • сделать выводы о текущей ситуации в онлайн-магазине, основываясь на выявленных показателях;
  3. Провести сегментацию пользователей на основе истории их покупок:
    • определить профиль пользователей: как "выглядит" наш типичный пользователь (что покупает, как часто, возвращается за повторной покупкой)
    • провести RFM-анализ (разделить покупателей на группы, основываясь на количестве покупок, среднем чеке, дате последней покупки).
    • определить, какая доля людей делает крупные покупки, какая - мелкие.
    • выделить основные товарные категории
    • на основе товарных категорий и заказов, определить сегментацию пользователей.
    • выявить какую выручку приносит компании каждый сегмент потребителей
  4. Проверить статистические гипотезы (в ходе исследования формулировки могут меняться и дополняться):
    • Средняя выручка между сегментами пользователей отличается и это отличие является статистически значимым.
    • Средний чек между сегментами пользователей отличается и это отличие является статистически значимым.
  5. На базе проведенного исследования подготовить рекомендации для бизнеса по работе с каждым из выявленных сегментов пользователей.

Описание данных:

  • date — дата заказа;
  • customer_id — идентификатор покупателя;
  • order_id — идентификатор заказа;
  • product — наименование товара;
  • quantity — количество товара в заказе;
  • price — цена товара.

Предобработка данных¶

In [1]:
# импортируем библиотеки
import pandas as pd
import matplotlib.pyplot as plt
from datetime import timedelta
import scipy.stats as stats

# импорты plotly
import plotly.graph_objs as go
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.io as pio
from IPython.display import Image, display
In [2]:
# загружаем данные
df = pd.read_csv('C:/anaconda/Datasets/ecom_dataset_upd.csv')
In [3]:
def check_dataset(dataset):
# выводим общую информацию о датасете
    print("Информация о датасете:")
    print("--------------------")
    dataset.info()
    print("\n")

    print("Описание данных:")
    print("--------------------")
    print(dataset.describe())
    print("\n")
    
# проверяем наличие дубликатов в таблицах
    print("Поиск явных дубликатов:")
    print("--------------")
    duplicates = dataset.duplicated()
    if duplicates.any():
        print(dataset[duplicates])
    else:
        print("Дубликаты не найдены")
    print("\n")
    
# проверяем данные на наличие пропусков
    print("Наличие пропусков в датасете (NaN):")
    print("---------------------")
    missing_values = dataset.isna().sum()
    if missing_values.any():
        print(missing_values)
    else:
        print("Пропуски отсутствуют (NaN)")
    print("\n")
In [4]:
check_dataset(df)
Информация о датасете:
--------------------
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7474 entries, 0 to 7473
Data columns (total 6 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   date         7474 non-null   int64  
 1   customer_id  7474 non-null   object 
 2   order_id     7474 non-null   int64  
 3   product      7474 non-null   object 
 4   quantity     7474 non-null   int64  
 5   price        7474 non-null   float64
dtypes: float64(1), int64(3), object(2)
memory usage: 350.5+ KB


Описание данных:
--------------------
               date       order_id     quantity         price
count  7.474000e+03    7474.000000  7474.000000   7474.000000
mean   2.018913e+09   49449.884265     2.362858    478.737501
std    4.278878e+05   32679.642404    14.500497    901.615895
min    2.018100e+09   12624.000000     1.000000      9.000000
25%    2.019022e+09   14833.000000     1.000000     97.000000
50%    2.019051e+09   68768.000000     1.000000    135.000000
75%    2.019063e+09   71257.750000     1.000000    439.000000
max    2.020013e+09  112789.000000  1000.000000  14917.000000


Поиск явных дубликатов:
--------------
Дубликаты не найдены


Наличие пропусков в датасете (NaN):
---------------------
Пропуски отсутствуют (NaN)


  1. При первичном анализе датасета дубликатов не обнаружено, пропусков в данных нет.
  2. Всего в датасете представлено 7474 строки, 6 колонок.
  3. в разделе описания данных можно заметить, что в колонках quantity и price возможно встречаются аномальные значения, требующие внимания. Максимальное значение кол-ва товаров - 1000 шт. при медианном в 1 шт. Максимальное значение цены товара - 14,9 тыс. рублей при медианном значении - 439 рублей. Далее дополнительно построим гистограмму распределения данных.
  4. Требуется изменить тип данных в колонке date.
In [5]:
# заменим формат данных в колонке `date`
df['date'] = pd.to_datetime(df['date'], format='%Y%m%d%H')

# создадим отдельный столбец только с датой (без времени)
df['dt'] = pd.to_datetime(df['date']).dt.date

# создадим отдельный столбец только с месяцем заказа
df['month'] = pd.to_datetime(df['date']).dt.strftime('%Y-%m')
In [6]:
# приводим данные к нижнему регистру
df['product'] = df['product'].str.lower()
In [7]:
# посмотрим общее распределение количественных данных в колонках
df.hist(figsize=(15, 15))
plt.suptitle('Общее распределение данных в колонках')
plt.show()
  1. Как мы отмечали выше, данные в столбцах quantity и price распределены неравномерно.
  2. Основная масса значений в колонке quantity находится в пределах до 2 шт. Аномальные значения достигают отметки в 1000 шт.
  3. Распределение в колонке price чуть более равномерное, но все ещё имеет длинный хвост справа, достигая максимальных значений до 15 тыс. рублей.

В рамках исследовательского анализа данных мы дополнительно проверим, какую долю аномальных значений требуется отрубить.

Дополнительно проверим датасет на наличие неявных дубликатов. Если они и есть, они должны иметь одинаковый id заказа, id клиента и название товара. В остальных случаях это скорее всего другой заказ.

In [8]:
# проверим датасет на наличие неявных дубликатов
pd.set_option('display.max_colwidth', None) 

df_duplicated = df[df.duplicated(['product', 'customer_id', 'order_id', 'quantity', 'price'])]
display(df_duplicated.sort_values (by = ['customer_id', 'order_id', 'product']).head(30))
len(df_duplicated)
date customer_id order_id product quantity price dt month
2241 2019-03-07 11:00:00 0184f535-b60a-4914-a982-231e3f615206 14566 пеларгония розебудная margaretha укорененный черенок 1 135.0 2019-03-07 2019-03
2242 2019-03-07 11:00:00 0184f535-b60a-4914-a982-231e3f615206 14566 пеларгония розебудная mary укорененный черенок 1 135.0 2019-03-07 2019-03
2243 2019-03-07 11:00:00 0184f535-b60a-4914-a982-231e3f615206 14566 пеларгония розебудная prins nikolai укорененный черенок 1 135.0 2019-03-07 2019-03
2244 2019-03-07 11:00:00 0184f535-b60a-4914-a982-231e3f615206 14566 пеларгония розебудная red pandora укорененный черенок 1 135.0 2019-03-07 2019-03
2891 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 бакопа ампельная махровая сиреневая махровая объем 0,5 л 1 90.0 2019-04-16 2019-04
2892 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 бакопа ампельная махровая фиолетовая махровая объем 0,5 л 1 90.0 2019-04-16 2019-04
2893 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 газания рассада однолетних цветов в кассете по 6 шт 1 210.0 2019-04-16 2019-04
2894 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 калибрахоа aloha double purple сиреневая махровая объем 0,5 л 1 90.0 2019-04-16 2019-04
2895 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 калибрахоа aloha tiki neon малиновая объем 0,5 л 1 90.0 2019-04-16 2019-04
2896 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 калибрахоа bloomtastic blossom розово-сиреневая объем 0,5 л 1 90.0 2019-04-16 2019-04
2897 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 калибрахоа mini famous double red красная махровая объем 0,5 л 1 90.0 2019-04-16 2019-04
2898 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 калибрахоа rave violet сиреневая звезда объем 0,5 л 1 90.0 2019-04-16 2019-04
2899 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 калибрахоа sweet bells double golden желтая махровая объем 0,5 л 1 90.0 2019-04-16 2019-04
2900 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 львиный зев рассада однолетних цветов в кассете по 6 шт 1 128.0 2019-04-16 2019-04
2902 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 петуния surprice yellow желтая с прожилками объем 0,5 л 1 90.0 2019-04-16 2019-04
2903 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 петуния sweetunia black satin черная объем 0,5 л 1 90.0 2019-04-16 2019-04
2904 2019-04-16 16:00:00 0184f535-b60a-4914-a982-231e3f615206 14649 томата (помидор) баскью блю №7 сорт детерминантный среднеспелый синий 1 38.0 2019-04-16 2019-04
6086 2019-08-13 09:00:00 019ddfb4-f9fe-4b17-88bb-0ec9edb56479 72274 сумка-тележка twin стальной каркас 56 л серая, gimi 1 2549.0 2019-08-13 2019-08
1276 2018-12-20 12:00:00 028469c0-9e87-4596-ac2e-c5b1d48ea9b6 69421 крючок одежный двойной усиленный алюминиевый (дюраль), 1110015 30 26.0 2018-12-20 2018-12
1433 2019-01-03 21:00:00 028469c0-9e87-4596-ac2e-c5b1d48ea9b6 69421 крючок одежный двойной усиленный алюминиевый (дюраль), 1110015 30 26.0 2019-01-03 2019-01
2038 2019-02-26 12:00:00 036edc2c-d0ad-4c71-99f6-226db1b883f4 70463 салатник luminarc поэма анис 12 см j1349 2 239.0 2019-02-26 2019-02
5489 2019-06-19 21:00:00 03865a43-8c19-4d4e-ab51-7ec516614a83 14870 пеларгония розебудная prins nikolai укорененный черенок 1 135.0 2019-06-19 2019-06
5490 2019-06-19 21:00:00 03865a43-8c19-4d4e-ab51-7ec516614a83 14870 пеларгония розебудная queen ingrid укорененный черенок 1 135.0 2019-06-19 2019-06
6472 2019-09-29 19:00:00 05f74c6f-2395-45ac-a826-9e070652de3e 72786 коврик придверный apache 45х76 см flagstone 5415 1 1199.0 2019-09-29 2019-09
2021 2019-02-25 15:00:00 075873aa-644c-4a09-9253-204f3156ac7b 70438 ёрш унитазный с деревянной ручкой , ваир 1712012 20 56.0 2019-02-25 2019-02
3964 2019-05-21 05:00:00 08d1c36d-1a94-4040-9cfe-f78a8e382a4a 71479 сумка-тележка 2-х колесная gimi argo синяя 1 1087.0 2019-05-21 2019-05
5991 2019-07-29 14:00:00 09521bde-42aa-482f-a92d-cf82b082bc82 72124 муляж долька арбуза 14*7,5 см 2 59.0 2019-07-29 2019-07
5993 2019-07-29 21:00:00 09521bde-42aa-482f-a92d-cf82b082bc82 72124 муляж долька арбуза 14*7,5 см 2 59.0 2019-07-29 2019-07
1508 2019-01-14 08:00:00 0982f6b9-328f-4a67-b7ee-cd0a114868f0 69807 контейнер для свч полимербыт премиум 1,2 л 4356200 1 59.0 2019-01-14 2019-01
4740 2019-06-06 23:00:00 09bcc3d0-8134-4f00-8ea5-b74b55d766ad 71633 стремянка scab balzo 762 5 ступеней алюминиевая 3885 1 5549.0 2019-06-06 2019-06
Out[8]:
1864
  1. Всего мы нашли 1864 неявных дубликата.
  2. Если обратить внимание на таблицу, можно заметить, что каждой строке соответствует в среднем 6 дубликатов товара с разницей в дате события. Наше предположение, что в дубликаты возникли из-за обрезанной выгрузки логов - не хватает регистрации событий внутри интернет-магазина (открыл карточку товара -- добавил в корзину -- оплатил товар и т.д.).
  3. Так как в рамках данного исследования нас интересует лишь факт покупки, мы удалим неявные дубликаты и оставим последнее событие - когда была осуществлена оплата товара.
In [9]:
# удаляем неявные дубликаты
df = df.sort_values('date').drop_duplicates(['product', 'customer_id', 'order_id', 'quantity', 'price'], keep='last')
In [10]:
# добавим для удобства столбец со стоимостью товаров
df['total_cost'] = df['price'] * df['quantity']
In [11]:
# проверим, что каждому заказу соответствует 1 дата и 1 пользователь
display(df.groupby('order_id').agg({'customer_id': 'nunique'}).query('customer_id > 1').count())
df.groupby('order_id').agg({'date': 'nunique'}).query('date > 1').count()
customer_id    29
dtype: int64
Out[11]:
date    54
dtype: int64

1 заказу в 28 случаях соответствует более 2 пользователей и в 54 случаях - более 2-х дат. Необходимо посмотреть, что это за заказы и принять решение по фильтрации.

In [12]:
double_cust = (df.pivot_table(index = 'order_id',
                              values = 'customer_id',
                              aggfunc = 'nunique')
               .query('customer_id > 1')
               .reset_index())
double_cust = double_cust['order_id'].to_list()
df.query('order_id in @double_cust').sort_values(by = ['order_id', 'customer_id', 'product'])
Out[12]:
date customer_id order_id product quantity price dt month total_cost
5545 2019-06-22 22:00:00 4e861452-b692-48dc-b756-99a130b7a70a 14872 однолетнее растение петуния махровая в кассете 4 шт, россия 2 82.0 2019-06-22 2019-06 164.0
5546 2019-06-22 22:00:00 4e861452-b692-48dc-b756-99a130b7a70a 14872 однолетнее растение петуния простая в кассете по 4 шт, россия 1 82.0 2019-06-22 2019-06 82.0
5547 2019-06-22 22:00:00 4e861452-b692-48dc-b756-99a130b7a70a 14872 петуния махровая рассада однолетних цветов в кассете по 6 шт 1 128.0 2019-06-22 2019-06 128.0
5549 2019-06-24 09:00:00 9897ccd6-9441-4886-b709-b06361fabf6c 14872 однолетнее растение петуния махровая в кассете 4 шт, россия 2 82.0 2019-06-24 2019-06 164.0
5550 2019-06-24 09:00:00 9897ccd6-9441-4886-b709-b06361fabf6c 14872 однолетнее растение петуния простая в кассете по 4 шт, россия 1 82.0 2019-06-24 2019-06 82.0
... ... ... ... ... ... ... ... ... ...
6508 2019-10-04 08:00:00 25a8cd52-3efa-48ee-a6bd-d413d7e2b42f 72845 муляж яблоко зеленый 9 см полиуретан 40 59.0 2019-10-04 2019-10 2360.0
6538 2019-10-07 20:00:00 2ac05362-3ca7-4d19-899c-7ba266902611 72845 муляж яблоко зеленый 9 см полиуретан 40 59.0 2019-10-07 2019-10 2360.0
6504 2019-10-03 14:00:00 d8465f63-35db-4809-aff3-a8f7ebfc257f 72845 муляж яблоко зеленый 9 см полиуретан 40 59.0 2019-10-03 2019-10 2360.0
6606 2019-10-14 09:00:00 2f1671cc-47eb-49bb-a40b-808375f4218b 72950 кастрюля эмалированная стэма с-1624 12 л цилиндрическая без рисунка 1506037 1 974.0 2019-10-14 2019-10 974.0
6601 2019-10-13 15:00:00 b1dbc7c4-3c84-40a7-80c9-46e2f79d24ad 72950 кастрюля эмалированная стэма с-1624 12 л цилиндрическая без рисунка 1506037 1 974.0 2019-10-13 2019-10 974.0

66 rows × 9 columns

In [13]:
double_date = (df.pivot_table(index = 'order_id',
                              values = 'date',
                              aggfunc = 'nunique')
               .query('date > 1')
               .reset_index())
double_date = double_date['order_id'].to_list()
df.query('order_id in @double_date').sort_values(by = ['order_id', 'customer_id', 'product'])
Out[13]:
date customer_id order_id product quantity price dt month total_cost
554 2018-10-31 13:00:00 3ee43256-af7d-4036-90d4-eeefa1afc767 14500 многолетнее растение душица-орегано розовый объем 0,5 л 1 89.0 2018-10-31 2018-10 89.0
555 2018-10-31 13:00:00 3ee43256-af7d-4036-90d4-eeefa1afc767 14500 многолетнее растение тимьян-чабрец розовый объем 0,5 л 1 89.0 2018-10-31 2018-10 89.0
556 2018-10-31 13:00:00 3ee43256-af7d-4036-90d4-eeefa1afc767 14500 пеларгония зональная диам. 12 см белая полумахровая 1 188.0 2018-10-31 2018-10 188.0
557 2018-10-31 13:00:00 3ee43256-af7d-4036-90d4-eeefa1afc767 14500 пеларгония зональная диам. 12 см розовая с малиновым полумахровая 1 188.0 2018-10-31 2018-10 188.0
558 2018-10-31 13:00:00 3ee43256-af7d-4036-90d4-eeefa1afc767 14500 пеларгония зональная диам. 12 см сиреневый полумахровый 1 188.0 2018-10-31 2018-10 188.0
... ... ... ... ... ... ... ... ... ...
6508 2019-10-04 08:00:00 25a8cd52-3efa-48ee-a6bd-d413d7e2b42f 72845 муляж яблоко зеленый 9 см полиуретан 40 59.0 2019-10-04 2019-10 2360.0
6538 2019-10-07 20:00:00 2ac05362-3ca7-4d19-899c-7ba266902611 72845 муляж яблоко зеленый 9 см полиуретан 40 59.0 2019-10-07 2019-10 2360.0
6504 2019-10-03 14:00:00 d8465f63-35db-4809-aff3-a8f7ebfc257f 72845 муляж яблоко зеленый 9 см полиуретан 40 59.0 2019-10-03 2019-10 2360.0
6606 2019-10-14 09:00:00 2f1671cc-47eb-49bb-a40b-808375f4218b 72950 кастрюля эмалированная стэма с-1624 12 л цилиндрическая без рисунка 1506037 1 974.0 2019-10-14 2019-10 974.0
6601 2019-10-13 15:00:00 b1dbc7c4-3c84-40a7-80c9-46e2f79d24ad 72950 кастрюля эмалированная стэма с-1624 12 л цилиндрическая без рисунка 1506037 1 974.0 2019-10-13 2019-10 974.0

207 rows × 9 columns

В обоих случаях не удалось найти единую логику возникновения ошибки и дублирования номера заказа. Так как мы не знаем, какой из заказов реальный, а какой - сбой, удалим обе опции.

In [14]:
df = df.query('order_id not in @double_date and order_id not in @double_cust')
df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 5401 entries, 0 to 7473
Data columns (total 9 columns):
 #   Column       Non-Null Count  Dtype         
---  ------       --------------  -----         
 0   date         5401 non-null   datetime64[ns]
 1   customer_id  5401 non-null   object        
 2   order_id     5401 non-null   int64         
 3   product      5401 non-null   object        
 4   quantity     5401 non-null   int64         
 5   price        5401 non-null   float64       
 6   dt           5401 non-null   object        
 7   month        5401 non-null   object        
 8   total_cost   5401 non-null   float64       
dtypes: datetime64[ns](1), float64(2), int64(2), object(4)
memory usage: 422.0+ KB

Проведена предобработка данных:

  1. Названия колонок приведены к нижнему регистру;
  2. Датасеты проверены на отсутствие явных и неявных дубликатов, пропусков в данных;
  3. Тип данных некоторых колонок приведен к нужному формату;
  4. Добавили несколько колонок для удобства дальнейшего анализа;

Исследовательский анализ данных¶

Ранее мы обратили внимание на хвосты в колонках quantity и price. Построим визуализацию, какие значения можно признать аномальными и удалить из датасета.

In [15]:
# построим box-plot для столбцов `quantity` и `price`
fig = make_subplots(rows=1, cols=2)

fig.add_trace(
    px.box(df, y='quantity').data[0],
    row=1, col=1
)

fig.add_trace(
    px.box(df, y='price').data[0],
    row=1, col=2
)
fig.update_yaxes(title_text="Количество штук в заказе", row=1, col=1)
fig.update_yaxes(title_text="Цена", row=1, col=2)

fig.update_layout(title_text="Распределение значений в столбцах quantity и price", title_x=0.5)
fig.show()
  1. В столбце quantity заметно, что основная доля значений - 1 штука. Все остальные значения - аномальные. Виден плотный хвост значений от 2 и до 100 штук.
  2. Медианная цена товаров - 150 рублей. Основная доля значений расположилась в пределах от 9 до 1162 рублей за товар. Есть и выбросы до 15 тыс. рублей, но скорее всего это лишь дорогостоящие товары, существующие в магазине.
  3. Дополнительно посмотрим, какую долю составляют аномальные значения в столбце quantity и посмотрим, что это за товары в датасете.
In [16]:
# определим 99-й перцентиль
df.quantity.quantile(0.99)
Out[16]:
26.0
In [17]:
df.query('quantity > 26').head(10)
Out[17]:
date customer_id order_id product quantity price dt month total_cost
13 2018-10-01 15:00:00 0948b0c2-990b-4a11-b835-69ac4714b21d 68486 крючок одежный 2-х рожковый серый металлик с полимерным покрытием *тонар*, 1110027 96 38.0 2018-10-01 2018-10 3648.0
144 2018-10-08 15:00:00 9151d307-654c-4239-a888-ada5ca45f0b2 68571 набор вешалок для костюма 45см 4шт цвет: кремовый, attribute, ahp224 37 202.0 2018-10-08 2018-10 7474.0
160 2018-10-09 09:00:00 c971fb21-d54c-4134-938f-16b62ee86d3b 68580 стяжка оконная с болтом ст-55 цинк, 1108354 64 19.0 2018-10-09 2018-10 1216.0
233 2018-10-12 15:00:00 4d93d3f6-8b24-403b-a74b-f5173e40d7db 68623 петля приварная гаражная d 14х90 мм с шаром, 1103003 50 38.0 2018-10-12 2018-10 1900.0
266 2018-10-16 08:00:00 cd09ea73-d9ce-48c3-b4c5-018113735e80 68611 крепеж для пружины дверной, 1107055 150 19.0 2018-10-16 2018-10 2850.0
267 2018-10-16 08:00:00 cd09ea73-d9ce-48c3-b4c5-018113735e80 68611 пружина дверная 240 мм оцинкованная (д-19 мм) без крепления, 1107014 150 38.0 2018-10-16 2018-10 5700.0
277 2018-10-16 22:00:00 42c5db22-6046-455b-a728-ff163a1b7808 68667 муляж красное яблоко мини полиуретан d-6 см 30 19.0 2018-10-16 2018-10 570.0
281 2018-10-17 13:00:00 4d93d3f6-8b24-403b-a74b-f5173e40d7db 68668 щетка для посуды *мила* sv3182 1807009 50 27.0 2018-10-17 2018-10 1350.0
282 2018-10-17 13:00:00 4d93d3f6-8b24-403b-a74b-f5173e40d7db 68668 щетка-утюжок с ручкой миди, standart, york, g1126 50 44.0 2018-10-17 2018-10 2200.0
568 2018-11-01 08:00:00 aa42dc38-780f-4b50-9a65-83b6fa64e766 68815 муляж яблоко 9 см красное 170 51.0 2018-11-01 2018-11 8670.0
In [18]:
# удалим тестовый заказ на 1000 единиц
df = df.query('quantity != 1000')

Общее количество покупок¶

In [19]:
print("Общее количество заказов:", len(df['order_id'].unique()))
Общее количество заказов: 3465

Общее количество уникальных пользователей¶

In [20]:
print("Общее количество пользователей:", len(df['customer_id'].unique()))
Общее количество пользователей: 2390

Определение оптимального периода для анализа¶

In [21]:
df['date'].describe(datetime_is_numeric=True)
Out[21]:
count                   5400
mean     2019-05-06 15:16:24
min      2018-10-01 00:00:00
25%      2019-01-30 20:00:00
50%      2019-04-29 13:00:00
75%      2019-08-02 11:15:00
max      2020-01-31 15:00:00
Name: date, dtype: object
In [22]:
display('Самая ранняя дата:',
       df['dt'].min())
display('Самая поздняя дата:',
       df['dt'].max())
print('Количество дней в датасете:', 
      str(df['dt'].max() - df['dt'].min()))
'Самая ранняя дата:'
datetime.date(2018, 10, 1)
'Самая поздняя дата:'
datetime.date(2020, 1, 31)
Количество дней в датасете: 487 days, 0:00:00

Дополнительно посмотрим динамику продаж в течение указанного времени. Для этого построим сводную таблицу с количеством заказов каждый день.

In [23]:
daily_sales = df.pivot_table(index='month', values ='order_id', aggfunc = 'nunique')
In [24]:
fig = px.line(daily_sales, y="order_id", title='Динамика продаж в интернет-магазине')

fig.update_yaxes(title_text="Количество заказов")
fig.update_xaxes(title_text="Месяц")
fig.update_layout(title_x=0.5)
fig.show()

Промежуточный вывод

  1. По графику можно заметить, что продажи в интернет-магазине распределены неравномерно. Бросаются в глаза 4 пика продаж - Декабрь 2018 года (273 заказа), Февраль 2019 года(280 заказов), Апрель 2019 года (261 заказ) и Январь 2020 года(269 заказов).

Общая выручка магазина¶

In [25]:
# посчитаем общую выручку интернет-магазина
revenue_by_month = (df.pivot_table(index = 'month',
                                   values = 'total_cost',
                                   aggfunc = 'sum')
                    .reset_index()
                   )
total_revenue = round(sum(revenue_by_month['total_cost']))

print("Общая выручка интернет-магазина:", total_revenue, "рублей")
Общая выручка интернет-магазина: 3861999 рублей
In [26]:
# построим визуализацию динамики продаж
fig = px.line(revenue_by_month, x = 'month',y="total_cost", title='Общая выручка')

fig.update_yaxes(title_text="Выручка, руб.")
fig.update_xaxes(title_text="Месяц")
fig.update_layout(title_x=0.5)
fig.show()

Промежуточный вывод

  1. При общей выручке в 3,8 млн. рублей, месячная выручка лишь 3 месяца достигала показатель 300 тыс. - в ноябре 2018, феврале и апреле 2019.
  2. Самые активные продажи произошли в конце 2018 - начале 2019 года. После пикового апреля можно заметить плавное снижение продаж, достигая самых низких показателей в ноябре - 128 тыс. рублей.
  3. В конце года месячная выручка снова выросла до 200 тыс.
  4. Владельцам бизнеса стоит дополнительно обратить внимание на общую выручку и принять меры по ее увеличению. Общая тенденция - уменьшение объемов продаж.

Средний чек покупателя¶

In [27]:
avg_profit = (df.pivot_table(index = ['dt', 'order_id'],
                                  values ='total_cost',
                                  aggfunc = 'sum')
              .reset_index()
             )
In [28]:
print("Средний чек покупателя:", round(avg_profit['total_cost'].mean(), 1), "руб.")
Средний чек покупателя: 1114.6 руб.
In [29]:
avg_profit_by_day = avg_profit.pivot_table(index ='dt', values ='total_cost', aggfunc = {'mean', 'median'}).reset_index()
avg_profit_by_day['rolling_mean'] = avg_profit_by_day['mean'].rolling(window =5).mean()
avg_profit_by_day['rolling_median'] = avg_profit_by_day['median'].rolling(window =5).mean()
avg_profit_by_day
Out[29]:
dt mean median rolling_mean rolling_median
0 2018-10-01 917.888889 674.0 NaN NaN
1 2018-10-02 1196.142857 1012.5 NaN NaN
2 2018-10-03 1321.750000 1254.5 NaN NaN
3 2018-10-04 2288.181818 1409.0 NaN NaN
4 2018-10-05 672.666667 351.5 1279.326046 940.3
... ... ... ... ... ...
479 2020-01-27 262.333333 89.0 1139.349784 714.6
480 2020-01-28 757.125000 225.0 1019.389069 650.2
481 2020-01-29 911.833333 880.5 977.512879 676.5
482 2020-01-30 339.666667 76.0 645.246212 291.5
483 2020-01-31 195.333333 128.0 493.258333 279.7

484 rows × 5 columns

In [30]:
# построим визуализацию динамику изменений среднего чека покупателей
fig = px.line(avg_profit_by_day,
              x = 'dt',
              y=['rolling_mean', 'rolling_median'],
              title='Средний чек покупателей',
              labels={
                  "value": "Стоимость покупки, руб.",
                  "dt": "Месяц",
                  "variable": "Тип чека"
              },
              )
fig.update_traces(
    name="Средний чек",
    selector=dict(name="rolling_mean")
)

fig.update_traces(
    name="Медианный чек", 
    selector=dict(name="rolling_median")
)

fig.update_layout(title_x=0.5)
fig.show()

Промежуточный вывод

  1. Средний чек покупателя составил 1114.6 руб
  2. Медианный чек ожидаемо стабильно ниже среднего чека пользователей.
  3. Обе линии имеют тенденцию к снижению. Если в конце 2018 - начале 2019 года средний чек находился в диапазоне от 1000 до 2500, то к концу 2019 года средний чек был в пределах 400-1000 рублей.

Среднее количество продуктов в заказе¶

In [31]:
# сгруппируем данные по номеру заказа
order_pivot = (df.pivot_table(index = 'order_id',
                              values = 'quantity',
                              aggfunc = {'count', 'sum'})
               .reset_index()
               )
In [32]:
print("Среднее количество позиций в заказах:", round(order_pivot['count'].mean()))
print("Среднее количество товаров в заказах:", round(order_pivot['sum'].mean()))
Среднее количество позиций в заказах: 2
Среднее количество товаров в заказах: 4

Самые популярные товары интернет-магазина¶

In [33]:
# проверим, какие товары покупают чаще всего 
top_products = (df.pivot_table(index ='product',
                values = 'order_id',
                aggfunc = 'nunique')
 .reset_index()
 .sort_values(by ='order_id',
              ascending= False)
 .head(15)
)

# построим визуализацию, какие товары покупают чаще всего
fig = px.bar(
    top_products,
    y='product',
   x='order_id',
    labels={'product': 'Название товара', 'order_id': 'Количество покупок'},
    title='Топ-15 востребуемых товаров интернет-магазина',
)

fig.update_layout(title_x=0.5, showlegend=True)
fig.update_layout(barmode='stack', yaxis={'categoryorder':'total ascending'})
fig.show()
  1. В конкретном случае мы хотели посмотреть на частоту покупок конкретных товаров, без привязки к количеству штук.
  2. Даже без предварительной категоризации товаров заметно, что из топ-15 товаров, абсолютное большинство - разновидности Пеларгонии (рассада). Видимо в нашем интернет-магазине очень выгодная цена на этот товар по сравнению с конкурентами.
  3. Помимо рассады, чаще всего у нас покупали сумки-тележки - 47 и 17 покупок соответственно.

Теперь посмотрим, какие товары покупали больше всего.

In [34]:
# проверим, какие товары покупают больше всего 
top_quantity_products = (df.pivot_table(index ='product',
                values = 'quantity',
                aggfunc = 'sum')
 .reset_index()
 .sort_values(by ='quantity',
              ascending= False)
 .head(15)
)

# построим визуализацию, какие товары покупают чаще всего
fig = px.bar(
    top_quantity_products,
    y='product',
   x='quantity',
    labels={'product': 'Название товара', 'quantity': 'Количество проданных единиц'},
    title='Топ-15 популярных товаров интернет-магазина',
)

fig.update_layout(title_x=0.5, showlegend=True)
fig.update_layout(barmode='stack', yaxis={'categoryorder':'total ascending'})
fig.show()
  1. Список топ-15 популярных товаров по количеству купленных единиц отличается от нашего предыдущего списка по частоте покупок. В лидерах теперь скорее товары для дома - муляжи, вешалки, плечики (618, 335, 160 единиц).
  2. Категория рассада совсем не встречается в нашем списке.

Самые прибыльные товары для компании¶

Ранее мы уде просмотрели, какие товары покупают больше и чаще всего. Вполне возможно, что основная выручка компании формируется за счет совсем других товарных позиций и именно на них нужно будет в будущем сделать упор в маркетинговой стратегии. Мы заметили, что чаще всего в нашем интернет-магазине покупают рассаду. Цена одной единицы - максимум 200 рублей. При этом мы также знаем, что в средняя цена товара в нашем магазине - 534 рубля.

Проведем минимальный ABC-анализ и посмотрим, какие товары генерируют 80% нашей выручки.

In [35]:
# проранжируем товары по количеству принесенной выручки
top_revenue_products = (df.pivot_table(index ='product',
                                            values = ['total_cost', 'quantity'],
                                            aggfunc = 'sum')
                        .reset_index()
                        .sort_values(by ='total_cost', ascending= False)
)
total_rev = top_revenue_products['total_cost'].sum()

# посчитаем, какой процент выручки приходится на каждый товар в ассортименте
top_revenue_products['share'] = top_revenue_products['total_cost'] / total_rev * 100
top_revenue_products = top_revenue_products.sort_values(by ='share', ascending= False)

# посчитаем кумулятивную сумму доли выручки
top_revenue_products['cum_sum'] = top_revenue_products['share'].cumsum()

# присвоим категории A-B-C для списка товаров, запишем в отдельный список
category_A_products = top_revenue_products.query('cum_sum < 80')['product'].to_list()
category_B_products = top_revenue_products.query('80 <= cum_sum < 95')['product'].to_list()
category_C_products = top_revenue_products.query('95 <= cum_sum')['product'].to_list()
In [36]:
# покажем разбивку товаров каждой категории
print('Общее кол-во товаров:',
      len(top_revenue_products))

print('Кол-во товаров, которые принесли 80% годовой выручки, Категория А:',
      len(top_revenue_products.query('cum_sum < 80')))

print('Доля товаров Категории А:',
      round(len(top_revenue_products.query('cum_sum < 80')) / 
            len(top_revenue_products) * 100), "%")
print("--------------")
print('Кол-во товаров, которые принесли 15% годовой выручки, Категория B:',
      len(top_revenue_products.query('80 <= cum_sum < 95')))

print('Доля товаров Категории В:',
      round(len(top_revenue_products.query('80 <= cum_sum < 95')) / 
            len(top_revenue_products) * 100), "%")

print("--------------")

print('Кол-во товаров, которые принесли 5% годовой выручки, Категория С:',
      len(top_revenue_products.query('95 <= cum_sum')))

print('Доля товаров Категории С:',
      round(len(top_revenue_products.query('95 <= cum_sum')) / 
            len(top_revenue_products) * 100), "%")
Общее кол-во товаров: 2300
Кол-во товаров, которые принесли 80% годовой выручки, Категория А: 613
Доля товаров Категории А: 27 %
--------------
Кол-во товаров, которые принесли 15% годовой выручки, Категория B: 683
Доля товаров Категории В: 30 %
--------------
Кол-во товаров, которые принесли 5% годовой выручки, Категория С: 1004
Доля товаров Категории С: 44 %
  1. Из всего списка товаров только 27% принесли 80% всей выручки. Данный список товаров можно отнести к категории "A". Такие товары выгоднее всего продавать и на их реализации можно будет в будущем сконцентрироваться.
  2. К категории "В" можно отнести товары, которые принесли компании - 15% всей выручки (кумулятивная доля 80-95%).
  3. К категории "С" можно отнести товары, которые принесли компании всего 5% всей выручки. В будущем нужно будет разбираться либо почему эти позиции так плохо продаются и корректировать стратегию продаж, либо выводить эти товары из реализации.
In [37]:
def categorize_product(df_row):
    if df_row['product'] in category_A_products:
        return "A"
    elif df_row['product'] in category_B_products:
        return "B"
    elif df_row['product'] in category_C_products:
        return "C"
    else:
        return "Unknown"

df['category_abc'] = df.apply(categorize_product, axis=1)
In [38]:
# проверим, какие товары покупают чаще всего с учетом категорий
top_products = (df.pivot_table(index =['product', 'category_abc'],
                values = 'order_id',
                aggfunc = 'nunique')
 .reset_index()
 .sort_values(by ='order_id',
              ascending= False)
 .head(15)
)

# построим визуализацию, какие товары покупают чаще всего
fig = px.bar(
    top_products,
    y='product',
   x='order_id',
    color = 'category_abc',
    labels={'product': 'Название товара', 'order_id': 'Количество покупок'},
    title='Топ-15 востребуемых товаров интернет-магазина',
)

fig.update_layout(title_x=0.5, showlegend=True)
fig.update_layout(barmode='stack', yaxis={'categoryorder':'total ascending'})
fig.show()
In [39]:
# проверим, какие товары покупают больше всего с учетом категорий
top_quantity_products = (df.pivot_table(index =['product', 'category_abc'],
                values = 'quantity',
                aggfunc = 'sum')
 .reset_index()
 .sort_values(by ='quantity',
              ascending= False)
 .head(15)
)

# построим визуализацию, какие товары покупают чаще всего
fig = px.bar(
    top_quantity_products,
    y='product',
   x='quantity',
    color = 'category_abc',
    labels={'product': 'Название товара', 'quantity': 'Количество проданных единиц'},
    title='Топ-15 популярных товаров интернет-магазина',
)

fig.update_layout(title_x=0.5, showlegend=True)
fig.update_layout(barmode='stack', yaxis={'categoryorder':'total ascending'})
fig.show()

Как и предполагалось, большинство наших самых популярных и востребованных товаров относятся к категории А.

Средняя частота покупок¶

In [40]:
frequency_user = df.pivot_table(index = 'customer_id', values = 'order_id', aggfunc = 'nunique')
frequency_user['order_id'].describe()
Out[40]:
count    2390.000000
mean        1.449791
std         2.710187
min         1.000000
25%         1.000000
50%         1.000000
75%         2.000000
max       126.000000
Name: order_id, dtype: float64

Стандартный покупатель интернет-магазина в среднем делает 1-2 покупки. Однако есть и по-настоящему лояльные покупатели, кто за год наблюдений совершил до 125 покупок.

In [41]:
print("Вернулись хотя бы 1 раз:", frequency_user.query('order_id > 1')['order_id'].count(), "покупателей")
print("Доля повторных покупателей:", round(frequency_user.query('order_id > 1')['order_id'].count() / frequency_user['order_id'].count() *100, 2))

print("Лояльная база интернет-магазина:", frequency_user.query('order_id > 2')['order_id'].count(), "покупателей")
Вернулись хотя бы 1 раз: 870 покупателей
Доля повторных покупателей: 36.4
Лояльная база интернет-магазина: 28 покупателей

Дополнительно посмотрим, сколько в среднем проходит дней между покупками.

In [42]:
df.sort_values(by=['customer_id', 'dt'], inplace=True)
df['time_difference'] = df.groupby('customer_id')['date'].diff()
df['time_difference'] = df['time_difference'].dt.days
df.query('time_difference > 0 ')['time_difference'].describe()
Out[42]:
count    864.000000
mean     221.261574
std      116.479057
min        1.000000
25%      145.000000
50%      262.000000
75%      304.000000
max      395.000000
Name: time_difference, dtype: float64
  1. В среднем между повторными покупками покупателей проходит 221 день. Медианное число - 262 дня. Это значит, что наш "типичный" покупатель, если и возвращается за повторной покупкой, то минимум через полгода, а то и дольше. Скорее всего его интересуют конкретные сезонные товары, которые он покупает каждый год.

В таком случае бизнесу стоило бы дополнительно проработать стратегию по удержанию пользователей. Проанализировать возможности проведения акций, которые бы участили визиты пользователей на сайт.

Общий вывод по разделу¶

  1. В нашем распоряжении оказались данные о 3465 заказах и 2390 уникальных пользователей. Период исследования - 1 октября 2018 - 31 января 2020 года.
  2. При общей выручке в 3,8 млн. рублей, месячная выручка лишь 3 месяца достигала показатель 300 тыс. - в ноябре 2018, феврале и апреле 2019. Самые активные продажи произошли в конце 2018 - начале 2019 года. После пикового апреля можно заметить плавное снижение продаж, достигая самых низких показателей в ноябре - 128 тыс. рублей. Общая тенденция - уменьшение объемов продаж.

  3. Средний чек покупателя в 2019 году составил 1114.6 руб. Общий тренд - снижение среднего чека в течение года. Если в конце 2018 - начале 2019 года средний чек находился в диапазоне от 1000 до 2500, то к концу 2019 года средний чек был в пределах 400-1000 рублей.

  4. Чаще всего в интернет-магазине покупают вариации рассады. Однако топ-15 товаров по количеству купленных единиц отличается от нашего предыдущего списка по частоте покупок. В лидерах теперь скорее товары для дома - муляжи, вешалки, плечики (618, 335, 160 единиц).

  5. Мы также провели ABC-анализ, который показал, что 27% товарного ассортимента магазина приносят компании 80% всей выручки (Категория А). Такие товары выгоднее всего продавать и на их реализации можно будет в будущем сконцентрироваться. 43% товарного ассортимента принесли компании всего 5% всей выручки. В будущем нужно будет разбираться либо почему эти позиции так плохо продаются и корректировать стратегию продаж, либо выводить эти товары из реализации. Мы сохранили категории товаров в отдельные списки, которые в дальнейшем можно будет передать маркетинговому отделу.

  6. Стандартный покупатель интернет-магазина в среднем делает 1-2 покупки.

  7. В среднем между повторными покупками покупателей проходит 221 день. Медианное число - 262 дня. Это значит, что наш "типичный" покупатель, если и возвращается за повторной покупкой, то минимум через полгода, а то и дольше.

Общие рекомендации для интернет-магазина

  1. обратить внимание на снижающиеся объемы продаж, а также на снижение среднего чека в течение всего года.
  2. Проанализировать товарный аcсортимент категории С - принять решение по стратегии продаж.
  3. Дополнительно проработать стратегию по удержанию пользователей. Проанализировать возможности проведения акций, которые бы участили визиты пользователей на сайт.

Сегментация пользователей¶

Ранее мы смотрели усредненные показатели пользователей: сколько в среднем наш покупатель делает покупок, сколько примерно тратит за 1 заказ, как быстро возвращается за повторной покупкой. Однако усредненный показатель не помогает строить более персонализированный подход к покупателям. Для этого нам потребуется для начала сегментировать покупателей на базе RFM-анализа.

RFM-анализ позволяет сегментировать клиентов по частоте и сумме покупок и выявлять тех клиентов, которые приносят больше денег. С каждой группой в дальнейшем можно строить отдельные коммуникации: давать им разную рекламу и делать разные email-рассылки.

RFM-анализ¶

In [43]:
# запишем максимальную дату анализа
snapshot_date = df['dt'].max() + timedelta(days=1)
print(snapshot_date)

# сгруппируем датафрейм по каждому пользователю
grouped_df = (df.groupby(['customer_id'])
              .agg({'dt': lambda x: (snapshot_date - x.max()).days,
                    'order_id': 'nunique',
                    'total_cost': 'sum'})
             )
# переименуем колонки
grouped_df.rename(columns={'dt': 'recency',
                           'order_id': 'frequency',
                           'total_cost': 'monetary'},
                  inplace=True)
2020-02-01
In [44]:
# выведем шапку обновленного датафрейма
print(grouped_df.head())
print('{:,} rows; {:,} columns'
      .format(grouped_df.shape[0], grouped_df.shape[1]))
                                      recency  frequency  monetary
customer_id                                                       
000d6849-084e-4d9f-ac03-37174eaf60c4      108          1     555.0
001cee7f-0b29-4716-b202-0042213ab038      350          1     442.0
00299f34-5385-4d13-9aea-c80b81658e1b      110          1     914.0
002d4d3a-4a59-406b-86ec-c3314357e498      370          1    1649.0
003bbd39-0000-41ff-b7f9-2ddaec152037      125          1    2324.0
2,390 rows; 3 columns
In [45]:
# определим, какое распределение частоты покупок в столбце frequency
grouped_df['frequency'].value_counts()
Out[45]:
1      1520
2       842
3        20
4         4
35        1
17        1
7         1
126       1
Name: frequency, dtype: int64
In [46]:
# определим функцию, которая вручную распределим баллы в зависимости от частоты покупок
def get_f(x):
    if x == 1:
        return 1
    if x in [2]:
        return 2
    if x in [3]:
        return 3
    return 4 
In [47]:
grouped_df['F'] = grouped_df['frequency'].apply(get_f)
In [48]:
# посмотрим, как сработало наше распределение
grouped_df.groupby('F')['frequency'].agg(['mean','count'])
Out[48]:
mean count
F
1 1.000 1520
2 2.000 842
3 3.000 20
4 25.125 8

Нам не удалось равномерно разделить колонку на 4 категории, так как практически половину записей о клиентах, которые совершили всего 1 покупку. Им мы выставили 1 балл. По остаточному принципу мы распределили 3 группы в зависимости от частоты их покупок.

In [49]:
# расчет показателя recency. Чем более недавно, тем лучше
grouped_df['R'] = 5 - (pd.qcut(grouped_df['recency'], 4, labels=False) + 1)
In [50]:
# расчет показателя monetary. Чем больше, тем лучше
grouped_df['M'] = pd.qcut(grouped_df['monetary'], 4, labels=False) + 1
In [51]:
# расчет общего rfm-score
grouped_df['rfm_score'] = grouped_df['R'] + grouped_df['F'] + grouped_df['M']
In [52]:
# функция для определения сегмента пользователей в зависимости от rfm-score
def rfm_level(df):
    if df['rfm_score'] >= 9:
        return 'Не можем их потерять'
    elif (df['rfm_score'] >= 8) and (df['rfm_score'] < 9):
        return 'Чемпионы'
    elif (df['rfm_score'] >= 7) and (df['rfm_score'] < 8):
        return 'Лояльные'
    elif (df['rfm_score'] >= 6) and (df['rfm_score'] < 7):
        return 'Потенциальные'
    elif (df['rfm_score'] >= 5) and (df['rfm_score'] < 6):
        return 'Перспективные'
    elif (df['rfm_score'] >= 4) and (df['rfm_score'] < 5):
        return 'Необходимо внимание'
    else:
        return 'Необходима активация'
In [53]:
grouped_df['segment'] = grouped_df.apply(rfm_level,axis=1)
grouped_df = grouped_df.reset_index()

# перенесем список id пользователей с присвоенным сегментом
champ_list = grouped_df.query('segment == "Чемпионы"')['customer_id'].to_list()
cant_lose_list = grouped_df.query('segment == "Не можем их потерять"')['customer_id'].to_list()
loyal_list = grouped_df.query('segment == "Лояльные"')['customer_id'].to_list()
potential_list = grouped_df.query('segment == "Потенциальные"')['customer_id'].to_list()
promising_list = grouped_df.query('segment == "Перспективные"')['customer_id'].to_list()
attention_list = grouped_df.query('segment == "Необходимо внимание"')['customer_id'].to_list()
activation_list = grouped_df.query('segment == "Необходима активация"')['customer_id'].to_list()

# сделаем запись о сегментах в основном дф
for index, row in df.iterrows():
    if row['customer_id'] in champ_list:
        df.at[index, 'segment'] = 'Чемпионы'
    elif row['customer_id'] in cant_lose_list:
        df.at[index, 'segment'] = 'Не можем их потерять'
    elif row['customer_id'] in loyal_list:
        df.at[index, 'segment'] = 'Лояльные'
    elif row['customer_id'] in potential_list:
        df.at[index, 'segment'] = 'Потенциальные'
    elif row['customer_id'] in promising_list:
        df.at[index, 'segment'] = 'Перспективные'
    elif row['customer_id'] in attention_list:
        df.at[index, 'segment'] = 'Необходимо внимание'
    elif row['customer_id'] in activation_list:
        df.at[index, 'segment'] = 'Необходима активация'    
In [54]:
rfm_level_agg = grouped_df.groupby('segment').agg({
    'recency': 'mean',
    'frequency': 'mean',
    'monetary': ['mean','sum', 'count']
}).round().reset_index()

rfm_level_agg.columns = [col[0] for col in rfm_level_agg.columns]
rfm_level_agg.columns = ['segment', 'recency', 'frequency', 'monetary', 'total_revenue', 'segment_count']
rfm_level_agg = rfm_level_agg.sort_values('total_revenue', ascending = False)
In [55]:
# построим общую визуализацию сегментов 
fig = px.treemap(rfm_level_agg,
                 path=['segment'],
                 hover_data = ['recency', 'frequency', 'segment_count'],
                 values='monetary',
                 title='Сегментация пользователей на базе RFM-анализа', 
                 labels={'segment': 'Название сегмента'})
fig.update_layout(title_x=0.5)

fig.show()
In [56]:
# построим визуализацию общей выручки, приносимой каждым сегментом пользователей
fig = px.bar(
    rfm_level_agg,
    y='total_revenue',
    x='segment',
    labels={'total_revenue': 'Объем выручки, млн. руб.', 'segment': 'Название RMF-сегмента'},
    title='Общая выручка интернет магазина с разбивкой по сегментам пользователей',
)

fig.add_trace(
    go.Scatter(x=rfm_level_agg['segment'], y=rfm_level_agg['segment_count'], mode='lines', name='Размер сегмента', yaxis='y2')
)
fig.update_layout(
    yaxis2=dict(overlaying='y',side='right'),
    title_x=0.5,
    showlegend=True,
    barmode='stack'
)

fig.show()
In [57]:
rfm_level_agg = rfm_level_agg.sort_values(by = 'monetary', ascending = False)
fig = px.bar(
    rfm_level_agg,
    y='monetary',
    x='segment',
    labels={'monetary': 'Средняя выручка с клиента, руб.', 'segment': 'Название RMF-сегмента'},
    title='Средняя выручка с пользователя интернет-магазина с разбивкой по сегментам пользователей',
)

fig.add_trace(
    go.Scatter(x=rfm_level_agg['segment'], y=rfm_level_agg['recency'], mode='lines', name='Средняя давность покупок', yaxis='y2')
)
fig.update_layout(
    yaxis2=dict(overlaying='y',side='right'),
    title_x=0.5,
    showlegend=True,
    barmode='stack'
)

fig.show()

Промежуточный вывод

  1. Мы провели RFM-анализ покупателей интернет-магазина. Благодаря анализу мы смогли сегментировать 7 групп пользователей, к каждой из которых можно применять персонализированный подход в продажах (подробнее в общих выводах).
  2. Благодаря графику общей выручки с разбивкой по сегментам, можно заметить, что практически половину всей выручки компании обеспечивает сегмент "Не можем их потерять" - 1,4 млн рублей. Меньше всего прибыли приносит сегмент "Необходима активация" - 32 тыс. рублей.
  3. Средняя выручка с клиента также пропорциональна выделенным сегментам. Больше всего тратят пользователи сегмента "Не можем их потерять" - в среднем 3 тыс. рублей. меньше всего "Необходима активация" - не более 200 рублей. Последние скорее всего пришли за одной мелкой покупкой и больше не возвращались.
  4. Показатель "recency" обратно пропорционален выделенным сегментам. Наши лидеры в среднем возвращаются за 52 дня за повторной покупкой, в то время как последний сегмент - более чем за 1 год.

Определение товарных категорий¶

In [58]:
# напишем функцию, которая выделяет первые 2 слова из колонки
def extract_category(input_string):
    words = input_string.split()
    return ' '.join(words[:3])
In [59]:
# применим функцию к нашему дф
df['product_new'] = df['product'].apply(extract_category)
df.head()
Out[59]:
date customer_id order_id product quantity price dt month total_cost category_abc time_difference segment product_new
6618 2019-10-16 08:00:00 000d6849-084e-4d9f-ac03-37174eaf60c4 14943 пеларгония зональная диам. 12 см темнорозовая полумахровая 1 150.0 2019-10-16 2019-10 150.0 A NaN Потенциальные пеларгония зональная диам.
6619 2019-10-16 08:00:00 000d6849-084e-4d9f-ac03-37174eaf60c4 14943 пеларгония розебудная queen ingrid укорененный черенок 1 135.0 2019-10-16 2019-10 135.0 A 0.0 Потенциальные пеларгония розебудная queen
6620 2019-10-16 08:00:00 000d6849-084e-4d9f-ac03-37174eaf60c4 14943 пеларгония розебудная rosebud red d-7 см 1 135.0 2019-10-16 2019-10 135.0 A 0.0 Потенциальные пеларгония розебудная rosebud
6621 2019-10-16 08:00:00 000d6849-084e-4d9f-ac03-37174eaf60c4 14943 пеларгония тюльпановидная emma 1 135.0 2019-10-16 2019-10 135.0 A 0.0 Потенциальные пеларгония тюльпановидная emma
1842 2019-02-16 11:00:00 001cee7f-0b29-4716-b202-0042213ab038 70290 сушилка для белья потолочная лиана люкс 150 см ллп-150 1 442.0 2019-02-16 2019-02 442.0 C NaN Необходимо внимание сушилка для белья
In [60]:
display(len(df['product'].unique()))
len(df['product_new'].unique())
2300
Out[60]:
1374

Таким образом мы убрали ненужные детали из описания товара - марку, артикул, размер и т.д. Количество уникальных названий сократилось с 2,2 тыс. до 1374 шт.

In [61]:
df['product_new'].value_counts()
Out[61]:
сушилка для белья                 292
пеларгония зональная диам.        272
рассада зелени для                226
герань домашняя (пеларгония       112
сумка-тележка 2-х колесная         98
                                 ... 
чехол для платья                    1
муляж перец болгарский              1
отбеливатель пероксоль лилия-м      1
салатник романтик гарден            1
чайник заварочный стеклянный        1
Name: product_new, Length: 1374, dtype: int64
In [62]:
# напишем функцию, которая определит названия товаров по категориям
def product_category(df_row):
    flowers_list = ['пеларгония','укроп','мимоза', 'вербейник','вербена','алиссум','хризантема','гардения', 'бегония', 'аптения', 'антуриум','азалия', 'тимьян','рукола', 'петрушк', 'капуста', 'гвоздика','афеляндра', 'хлорофит','бальзамин', 'арбуз', 'мята','колокольчик', 'дыня',  'цикламен', 'флокс', 'циперу','фиалка', 'примула', 'рассада','котовник','овсянниц', 'томат', 'помидор', 'пуансеттия', 'настурция', 'герань', 'фуксия','эвкалипт', 'растение', 'роза', 'бакопа', 'петуния', 'калибрахоа', 'базилик', 'зелен', 'огурец', 'горшк', 'кашпо']
    decor_list = ['искусственн','подарочн','декор','ваза', 'муляж', 'новогодн', 'штора', 'штор', 'карниз']
    bath_list = ['ванна','мыло', 'корзина', 'ванн', 'белья', 'ёрш','ерш','зубна', 'туалет', 'унитаз','стирк', 'мыла', 'таз', 'тряпкодержатель', 'вантуз']
    house_list = ['сушилка', 'ящик','ковёр', 'мытья','перчатки','окон', 'весы напольные', 'совок','веник','подрукавник', 'вешал','чистк', 'окномойка', 'ковер', 'одежды', 'коврик', 'чехол', 'гладильн', 'вешалк', 'плечик', 'кофр', 'щетк', 'швабра']
    kitchen_list = ['чайник','салфет','сито', 'нож','измельчитель', 'ложка','кухон', 'кухн', 'выпечк', 'посуд','бокал', 'масленка', 'сахар','скалк','миксер', 'соковыжималк', 'стакан', 'разделочн', 'бульон', 'блюдо', 'кастрюля', 'холодца', 'вилка', 'миска','блюдце', 'хлебница', 'чайный', 'муки', 'термос','фужер', 'салатн', 'банка', 'свч', 'салфетк', 'тарелка', 'кувшин', 'кружк', 'терка', 'овощеварка','сковород',  'продукт']
    textile_list = ['скатерть', 'простынь', 'ткань', 'полотен', 'одеяло', 'простын', 'халат', 'покрывал', 'плед']
    storage_list = ['сумка', 'тележк', 'стремянк','обув', 'крючок', 'мешок', 'хранен', 'ведро','ведр', 'коробка', 'лестница','прищеп', 'мусор','урна', 'полки', 'подставка', 'этажерка']
    
    for name in flowers_list:
        if name in df_row['product_new']:
            return 'Растения и рассада'

    for name in bath_list:
        if name in df_row['product_new']:
            return 'Товары для ванной'
        
    for name in decor_list:
        if name in df_row['product_new']:
            return 'Декор'
    
    for name in house_list:
        if name in df_row['product_new']:
            return 'Товары для дома'

    for name in kitchen_list:
        if name in df_row['product_new']:
            return 'Товары для кухни'        
        
    for name in textile_list:
        if name in df_row['product_new']:
            return 'Текстиль'

    for name in storage_list:
        if name in df_row['product_new']:
            return 'Хранение'
        
    return 'Другое'
In [63]:
# применяем функцию и создаем отдельный столбец с категориями
df['category'] = df.apply(product_category, axis = 1)
In [64]:
df['category'].value_counts()
Out[64]:
Растения и рассада    2446
Товары для ванной      621
Товары для дома        613
Хранение               529
Другое                 450
Товары для кухни       381
Декор                  295
Текстиль                65
Name: category, dtype: int64

По предварительному осмотру распределилось более 90% всего датасета. Оставшаяся часть перешла в категорию "Другое". После кластеризации становится заметно, что более половины всего датасета занимает категория "Растения и рассада".

Необходимо посмотреть, какие категории приносят интернет-магазину больше всего выручки

In [65]:
df_grouped = (df.pivot_table(index = ['category', 'category_abc'], values = 'total_cost', aggfunc = 'sum')
              .reset_index()
              .sort_values(by = ['total_cost', 'category'], ascending = False)
)

# строим визуализацию 
fig = px.bar(
    df_grouped,
    y='total_cost',
    x='category',
    color = 'category_abc',
    labels={'total_cost': 'Общая выручка, млн. руб.', 'category': 'Категории товаров', 'category_abc': 'Группы товаров'},
    title='Распределение выручки по категориям товаров',
)

fig.update_layout(title_x=0.5, showlegend=True)
fig.update_layout(barmode='stack', yaxis={'categoryorder':'total descending'})

fig.show()
  1. Больше всего выручки приносит категория Хранение - более 1,1 млн. рублей. И именно в этой категории больше всего прибыль от товаров группы А.
  2. Несмотря на то, что категория Растения и рассада представлена лучше всего в нашем датасете, прибыль от продаж товаров этой категории лишь на 4-м месте - суммарно около 480 тыс.
  3. Меньше всего прибыли поступает от категории Декор - не более 140 тыс. рублей.

Теперь посмотрим, как покупают товары ранее выделенные сегменты RFM-анализа.

In [66]:
df_grouped_1 = (df.pivot_table(index = ['segment', 'category'], values = 'total_cost', aggfunc = 'sum')
              .reset_index()
              .sort_values(by = ['total_cost', 'segment'], ascending = False)
)

# строим визуализацию 
fig = px.bar(
    df_grouped_1,
    y='total_cost',
    x='segment',
    color = 'category',
    labels={'total_cost': 'Общая выручка, млн. руб.', 'category': 'Категории товаров', 'segment': 'Сегментация пользователей'},
    title='Распределение выручки по сегментам пользователей',
)

fig.update_layout(title_x=0.5, showlegend=True)
fig.update_layout(barmode='stack', yaxis={'categoryorder':'total descending'})

fig.show()
  1. Как уже ранее отмечали, больше всего выручки приносит сегмент "Не можем их потерять" - суммарно около 1,4 млн. Чаще всего пользователи этого сегмента покупать товары категории Хранение (567 тыс.), Товары для дома (321 тыс.) и Товары для ванной (265 тыс.). Меньше всего покупают Декор - не более 18 тыс. рублей.
  2. Чуть менее выраженный характер потребления у сегмента Чемпионы. Здесь по-прежнему лидирует категория Хранение (171 тыс.), однако остальные категории менее выраженные - практически равные доли у Товаров для Дома и Ванной - 90 и 99 тыс. рублей соответственно. Аналогично у категорий Растения и Рассада, Текстиль и Другое - порядка 50 тыс. рублей на каждую категорию.
  3. Более однородный запрос у сегмента Перспективные. Здесь категории распределились практически равномерно. В порядке убывания - Растения и Рассада, Товары для дома, Товары для ванной, Хранение (все по 60-50 тыс.) и др.
In [67]:
# посмотрим сезонность продаж разных категорий товаров
season = (df.pivot_table(index = ['month', 'category'],
                        values = 'total_cost',
                        aggfunc = 'sum')
          .reset_index()
        )

# построим визуализацию
fig = px.line(
    season,
    y='total_cost',
    x='month',
    color = 'category',
    labels={'total_cost': 'Общая выручка, руб.', 'month': 'Месяц', 'category': 'Категория'},
    title='Сезонность продаж категорий товаров',
)

fig.update_layout(title_x=0.5, showlegend=True)

fig.show()
  1. Категория Хранение лидирует практически во все месяцы за исключением Января 2019, и весны 2019 года (апрель - июнь). Каждый месяц продажи категории хранения приносят от 40 до 110 тыс. рублей.
  2. Заметен сильный всплеск продаж категории Растения и рассада, начиная с марта 2019 и до июня 2019 года. Это можно легко объяснить сезоном высадки рассады. Пик продаж приходится на май - выручка более 100 тыс. рублей.
  3. В июне 2019 года заметен сильный всплеск продаж товаров категории Товары для дома, выводя категорию на первое место по продажам в тот месяц - суммарная выручка 82 тыс. рублей. Назвать это сезонностью сложно. Возможно проводилась маркетинговая акция.
  4. Остальные сегменты хоть и распределены неравномерно в течение года, однако нет возможности выделить характерный всплеск или сезонность.

Общий вывод¶

  1. Мы провели RFM-анализ покупателей интернет-магазина. Благодаря анализу мы смогли сегментировать 7 групп пользователей, к каждой из которых можно применять персонализированный подход в продажах:

    • Сегмент "Не можем их потерять" - стратегически важные клиенты. Они регулярно совершают покупки на крупные суммы. Стратегия: можем направить им особые предложения, участие в программе лояльности.

    • Сегмент "Чемпионы" - регулярные покупатели. Стратегия: подготовить программу лояльности, начислить бонусы, предложить подарок за покупку или достижение конкретного уровня чека.

    • Сегмент "Лояльные" - взаимодействуют с компанией нерегулярно, но при этом тратят большие суммы. Стратегия: необходимо изучить потребности клиентов и направить персонализированные предложения. Таким клиентам не рекомендуется предлагать скидки.
    • Сегмент "Потенциальные" - такие покупатели помнят о компании, когда-то совершали покупки, но сейчас перестали это делать. Стратегия: необходимо напомнить о компании посредством рассылки, разработки стимулирующих акций, начисления бонусов на карту лояльности.
    • Сегмент "Перспективные" - при правильном взаимодействии могут стать постоянными клиентами. Стратегия: Этому сегменту нужно оказывать активную помощь в выборе товара, давать полезную информацию.
    • Сегмент "Необходимо внимание" - необходимо получить обратную связь, чтобы понять причину отказа от приобретения товаров. Можно начислить бонусы и предложить выгодную акцию. Таких покупателей можно вернуть и сделать постоянными клиентами.
    • Сегмент "Необходима активация" - клиенты давно не взаимодействуют с компанией. Стратегия: Можно восстановить контакт с помощью рассылки. Настаивать и тратить много времени/сил не рекомендуется.
  2. Разбивка по сегментам демонстрирует, что практически половину всей выручки компании обеспечивает сегмент "Не можем их потерять" - 1,4 млн рублей. Меньше всего прибыли приносит сегмент "Необходима активация" - 32 тыс. рублей.

  3. Средняя выручка с клиента также пропорциональна выделенным сегментам. Больше всего тратят пользователи сегмента "Не можем их потерять" - в среднем 3,3 тыс. рублей. меньше всего "Необходима активация" - не более 200 рублей. Последние скорее всего пришли за одной мелкой покупкой и больше не возвращались.
  4. Дополнительно выделили товарные категории ассортимента интернет-магазина - всего 8 категорий. Больше всего выручки приносит категория Хранение - более 1,1 млн. рублей. И именно в этой категории больше всего прибыль от товаров группы А. Меньше всего прибыли поступает от категории Декор - не более 140 тыс. рублей. Возможно стоит увеличить товарный ассортимент этой категории.
  5. Проанализировали поведение пользователей каждого сегмента - какие товары пользователи покупают больше всего.

    • Сегмент "Не можем их потерять" предпочитают покупать товары категории Хранение, Товары для дома и Товары для ванной. Меньше всего покупают Декор - не более 18 тыс. рублей.
    • Сегмент "Чемпионы" также предпочитает товары категории Хранение, однако остальные категории распределены более равномерно - практически равные доли у Товаров для Дома и Ванной (90 и 99 тыс.). Растения и Рассада, Текстиль и Другое - порядка 50 тыс. рублей на каждую категорию.
    • Более однородный запрос у сегмента "Перспективные". Здесь категории распределились практически равномерно. В порядке убывания - Растения и Рассада, Товары для дома, Товары для ванной, Хранение (все по 60-50 тыс.) и др.
  6. Проанализировали динамику продаж товаров разных категорий. Выявили яркую сезонность у категории Растения и рассада - пик продаж приходится на март - июнь. Остальные сегменты хоть и распределены неравномерно в течение года, однако нет возможности выделить характерный всплеск или сезонность.

Проверка статистических гипотез¶

Для того чтобы вынести окончательные рекомендации для интернет-магазине, необходимо убедиться, что те различия, которые мы идентифицировали между сегментами пользователей действительно статистически значимы. Для этого мы проведем несколько стат. тестов и сравним сегменты между собой.

Так как перед нами сразу несколько групп для сравнения (сегменты пользователей), мы можем провести тест ANOVA, который используется для сравнения средних значений двух или более выборок.

Какие гипотезы мы будем проверять?

  1. Средняя выручка между сегментами пользователей отличается и это отличие является статистически значимым.
  2. Средний чек между сегментами пользователей отличается и это отличие является статистически значимым.

Этапы проверки гипотез:

  1. Группировка данных для проведения анализа
  2. Проведения теста на нормальность, чтобы проверить, являются ли данные нормально распределенными в каждой из групп.
  3. Формулировка нулевой и альтернативной гипотез
  4. Проведение теста и интерпретация результатов.

Группировка данных для проведения анализа¶

Сгруппируем изначальный датафрейм такис образом, чтобы для каждого уникального пользователя отображался средний чек, общая стоимость покупок, а также выделенный сегмент (на базе RFM-анализа).

In [68]:
# создаем датафрейм для сегмента Не можем их потерять
df_1 = (df.query('segment == "Не можем их потерять"')
        .groupby('customer_id')
        .agg({'order_id': 'nunique',
              'total_cost': 'sum'})
        .round()
        .reset_index()
        .sort_values(by = 'total_cost')
       )
df_1['mean_order'] = df_1['total_cost'] / df_1['order_id']

# создаем датафрейм для сегмента Чемпионы
df_2 = (df.query('segment == "Чемпионы"')
        .groupby('customer_id')
        .agg({'order_id': 'nunique',
              'total_cost': 'sum'})
        .round()
        .reset_index()
        .sort_values(by = 'total_cost')
       )
df_2['mean_order'] = df_2['total_cost'] / df_2['order_id']

# создаем датафрейм для сегмента Лояльные
df_3 = (df.query('segment == "Лояльные"')
        .groupby('customer_id')
        .agg({'order_id': 'nunique',
              'total_cost': 'sum'})
        .round()
        .reset_index()
        .sort_values(by = 'total_cost')
       )
df_3['mean_order'] = df_3['total_cost'] / df_3['order_id']

# создаем датафрейм для сегмента Потенциальные
df_4 = (df.query('segment == "Потенциальные"')
        .groupby('customer_id')
        .agg({'order_id': 'nunique',
              'total_cost': 'sum'})
        .round()
        .reset_index()
        .sort_values(by = 'total_cost')
       )
df_4['mean_order'] = df_4['total_cost'] / df_4['order_id']

# создаем датафрейм для сегмента Перспективные
df_5 = (df.query('segment == "Перспективные"')
        .groupby('customer_id')
        .agg({'order_id': 'nunique',
              'total_cost': 'sum'})
        .round()
        .reset_index()
        .sort_values(by = 'total_cost')
       )
df_5['mean_order'] = df_5['total_cost'] / df_5['order_id']

# создаем датафрейм для сегмента Необходимо внимание
df_6 = (df.query('segment == "Необходимо внимание"')
        .groupby('customer_id')
        .agg({'order_id': 'nunique',
              'total_cost': 'sum'})
        .round()
        .reset_index()
        .sort_values(by = 'total_cost')
       )
df_6['mean_order'] = df_6['total_cost'] / df_6['order_id']

# создаем датафрейм для сегмента Необходима активация
df_7 = (df.query('segment == "Необходима активация"')
        .groupby('customer_id')
        .agg({'order_id': 'nunique',
              'total_cost': 'sum'})
        .round()
        .reset_index()
        .sort_values(by = 'total_cost')
       )
df_7['mean_order'] = df_7['total_cost'] / df_7['order_id']
In [69]:
# определяем нормальность распределения для колонки среднего чека 
all_segments_names = {
    1: df_1,
    2: df_2,
    3: df_3,
    4: df_4,
    5: df_5,
    6: df_6,
    7: df_7,
}

alpha = 0.05

for i, df_i in all_segments_names.items():
    stat, p = stats.shapiro(df_i['mean_order'])
    
    if p < alpha:
        print('Statistics=%.3f, p-value=%.3f' % (stat, p))
        print('Отклонить гипотезу о нормальности распределения')
        print(" ")
    else:
        print('Statistics=%.3f, p-value=%.3f' % (stat, p))
        print('Принять гипотезу о нормальности распределения')
        print(" ")
Statistics=0.738, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.645, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.355, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.432, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.929, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.945, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.944, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
In [70]:
# определяем нормальность распределения для колонки общей выручки с покупателя
for i, df_i in all_segments_names.items():
    stat, p = stats.shapiro(df_i['total_cost'])
    
    if p < alpha:
        print('Statistics=%.3f, p-value=%.3f' % (stat, p))
        print('Отклонить гипотезу о нормальности распределения')
        print(" ")
    else:
        print('Statistics=%.3f, p-value=%.3f' % (stat, p))
        print('Принять гипотезу о нормальности распределения')
        print(" ")
Statistics=0.173, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.621, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.287, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.424, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.934, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.948, p-value=0.000
Отклонить гипотезу о нормальности распределения
 
Statistics=0.944, p-value=0.000
Отклонить гипотезу о нормальности распределения
 

Во всех проверках на нормальность распределения мы получили отрицательный результат. В таком случае мы не можем использовать параметрический тест ANOVA. Непараметрическая альтернатива такому тесту - критерий Краскела-Уоллиса.

Формулировка нулевой и альтернативной гипотез¶

Проверка № 1 - Средняя выручка между сегментами пользователей отличается и это отличие является статистически значимым.

Тест Краскела-Уоллиса является ранговым и проводит сравнение не по среднему показателю, а по медианному. В таком случае гипотезы будут звучать следующим образом:

Нулевая гипотеза (H0): Медианная выручка одинакова для всех сегментов.

Альтернативная гипотеза: (Ha): Медианная выручка не одинакова для всех сегментов и это отличие является статистически значимым.

Проверка № 2 - Средний чек между сегментами пользователей отличается и это отличие является статистически значимым.

Нулевая гипотеза (H0): Медианная чек одинаков для всех сегментов пользователей.

Альтернативная гипотеза: (Ha): Медианная чек не одинаков для всех сегментов и это отличие является статистически значимым.

Проведение теста и интерпретация результатов.¶

In [71]:
# проверка гипотезы о равенстве медианной выручки
stat, p = stats.kruskal(df_1['total_cost'],
                        df_2['total_cost'],
                        df_3['total_cost'],
                        df_4['total_cost'],
                        df_5['total_cost'],
                        df_6['total_cost'],
                        df_7['total_cost'])
if p < alpha:
        print('Statistics=%.2f, p-value=%.3f' % (stat, p))
        print('Отвергаем нулевую гипотезу: между сегментами есть значимая разница')
        print(" ")
else:
        print('Statistics=%.2f, p-value=%.3f' % (stat, p))
        print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать медианную выручку между сегментами разной')
        print(" ")
Statistics=1069.02, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
In [72]:
# для проверки гипотезы о равенстве медианного чека
# необходимо дополнительно ранжировать датасеты по возрастанию среднего чека
df_1 = df_1.sort_values(by = 'mean_order')
df_2 = df_2.sort_values(by = 'mean_order')
df_3 = df_3.sort_values(by = 'mean_order')
df_4 = df_4.sort_values(by = 'mean_order')
df_5 = df_5.sort_values(by = 'mean_order')
df_6 = df_6.sort_values(by = 'mean_order')
df_7 = df_7.sort_values(by = 'mean_order')

# проверка гипотезы о равенстве медианного чека
stat, p = stats.kruskal(df_1['mean_order'],
                        df_2['mean_order'],
                        df_3['mean_order'],
                        df_4['mean_order'],
                        df_5['mean_order'],
                        df_6['mean_order'],
                        df_7['mean_order'])
if p < alpha:
        print('Statistics=%.2f, p-value=%.3f' % (stat, p))
        print('Отвергаем нулевую гипотезу: между сегментами есть значимая разница')
        print(" ")
else:
        print('Statistics=%.2f, p-value=%.3f' % (stat, p))
        print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать медианную выручку между сегментами разной')
        print(" ")
Statistics=600.86, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 

В обоих случаях нулевая гипотеза была опровергнута в пользу альтернативной. Таким образом, мы с достаточной долей уверенности можем утверждать, что между выделенными нами сегментами пользователей есть статистически значимая разница как в медианной выручке с пользователя, так и в медианном чеке с покупки.

Благодаря проверке мы можем утверждать, что сегменты имеют место быть. Осталось проверить, все ли сегменты имеют статистически значимую разницу между собой. Проведем попарные сравнения каждого сегмента. Гипотезы остаются те же.

Важно учесть, что мы проводим сразу несколько попарных тестов, поэтому важно применить поправку Бонферрони. Корректной стат. значимостью можно считать следующую: alpha / "кол-во экспериментов" = 0,05 / 42 = 0.00119048

In [73]:
# проверка гипотезы о равенстве средней выручки между парами сегментов
alpha = 0.00119048

for i, df_i in all_segments_names.items():
    name_i = 'Сегмент № ' + str(i)
    for j, df_j in all_segments_names.items():
        
        if i >= j:
            continue
            
        name_j = 'Сегмент № ' + str(j)
        stat, p = stats.ttest_ind(a=df_i['total_cost'], b=df_j['total_cost'], equal_var=False)
        print("Проверка гипотезы для пары:",  f"{name_i} and {name_j}")
        print('Statistics=%.2f, p-value=%.3f' % (stat, p))
        if p < alpha:
            print('Отвергаем нулевую гипотезу: между сегментами есть значимая разница')
        else:
            print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать среднюю выручку между сегментами разной')

        print(" ")
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 2
Statistics=3.48, p-value=0.001
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 3
Statistics=3.39, p-value=0.001
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 4
Statistics=3.51, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 5
Statistics=6.64, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 6
Statistics=7.30, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 7
Statistics=7.73, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 2 and Сегмент № 3
Statistics=0.26, p-value=0.797
Не получилось отвергнуть нулевую гипотезу, нет оснований считать среднюю выручку между сегментами разной
 
Проверка гипотезы для пары: Сегмент № 2 and Сегмент № 4
Statistics=0.14, p-value=0.891
Не получилось отвергнуть нулевую гипотезу, нет оснований считать среднюю выручку между сегментами разной
 
Проверка гипотезы для пары: Сегмент № 2 and Сегмент № 5
Statistics=8.96, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 2 and Сегмент № 6
Statistics=11.02, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 2 and Сегмент № 7
Statistics=12.34, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 3 and Сегмент № 4
Statistics=-0.14, p-value=0.885
Не получилось отвергнуть нулевую гипотезу, нет оснований считать среднюю выручку между сегментами разной
 
Проверка гипотезы для пары: Сегмент № 3 and Сегмент № 5
Statistics=5.34, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 3 and Сегмент № 6
Statistics=6.59, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 3 and Сегмент № 7
Statistics=7.41, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 4 and Сегмент № 5
Statistics=7.90, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 4 and Сегмент № 6
Statistics=9.73, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 4 and Сегмент № 7
Statistics=10.91, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 5 and Сегмент № 6
Statistics=10.41, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 5 and Сегмент № 7
Statistics=18.62, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 6 and Сегмент № 7
Statistics=10.92, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 

При попарном сравнении становится ясно, что не все сегменты имеют стат. значимые отличия между собой. Не удалось опровергнуть нулевую гипотезу в случае с сегментами 2, 3, 4 - Чемпионы, Лояльные и Потенциальные. Во всех остальных случаях между сегментами присутствует стат. значимая разница средней выручки.

Это значит, что мы всё ещё можем использовать нашу сегментацию в рамках подготовки рекомендаций для бизнеса, но с пометкой, что сегменты Чемпионы, Лояльные и Потенциальные возможно переформатировать на более укрупненные.

In [74]:
# проверка гипотезы о равенстве среднего чека между парами сегментов

for i, df_i in all_segments_names.items():
    name_i = 'Сегмент № ' + str(i)
    for j, df_j in all_segments_names.items():
        
        if i >= j:
            continue
            
        name_j = 'Сегмент № ' + str(j)
        stat, p = stats.ttest_ind(a=df_i['mean_order'], b=df_j['mean_order'], equal_var=False)
        print("Проверка гипотезы для пары:",  f"{name_i} and {name_j}")
        print('Statistics=%.2f, p-value=%.3f' % (stat, p))
        if p < alpha:
            print('Отвергаем нулевую гипотезу: между сегментами есть значимая разница')
        else:
            print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать средний чек между сегментами разным')

        print(" ")
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 2
Statistics=-0.17, p-value=0.865
Не получилось отвергнуть нулевую гипотезу, нет оснований считать средний чек между сегментами разным
 
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 3
Statistics=-1.12, p-value=0.262
Не получилось отвергнуть нулевую гипотезу, нет оснований считать средний чек между сегментами разным
 
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 4
Statistics=-2.73, p-value=0.007
Не получилось отвергнуть нулевую гипотезу, нет оснований считать средний чек между сегментами разным
 
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 5
Statistics=12.99, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 6
Statistics=18.46, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 1 and Сегмент № 7
Statistics=22.00, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 2 and Сегмент № 3
Statistics=-0.87, p-value=0.382
Не получилось отвергнуть нулевую гипотезу, нет оснований считать средний чек между сегментами разным
 
Проверка гипотезы для пары: Сегмент № 2 and Сегмент № 4
Statistics=-2.17, p-value=0.030
Не получилось отвергнуть нулевую гипотезу, нет оснований считать средний чек между сегментами разным
 
Проверка гипотезы для пары: Сегмент № 2 and Сегмент № 5
Statistics=6.45, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 2 and Сегмент № 6
Statistics=8.75, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 2 and Сегмент № 7
Statistics=10.25, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 3 and Сегмент № 4
Statistics=-0.99, p-value=0.320
Не получилось отвергнуть нулевую гипотезу, нет оснований считать средний чек между сегментами разным
 
Проверка гипотезы для пары: Сегмент № 3 and Сегмент № 5
Statistics=5.38, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 3 and Сегмент № 6
Statistics=6.90, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 3 and Сегмент № 7
Statistics=7.89, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 4 and Сегмент № 5
Statistics=7.75, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 4 and Сегмент № 6
Statistics=9.53, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 4 and Сегмент № 7
Statistics=10.69, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 5 and Сегмент № 6
Statistics=10.06, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 5 and Сегмент № 7
Statistics=18.11, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 
Проверка гипотезы для пары: Сегмент № 6 and Сегмент № 7
Statistics=10.71, p-value=0.000
Отвергаем нулевую гипотезу: между сегментами есть значимая разница
 

Не удалось найти стат. значимые отличия в среднем чеке пользователей сегментов 1, 2, 3 и 4 - Не можем их потерять, Чемпионы, Лояльные и Потенциальные. В остальных случаях средний чек отличается между сегментами и это отличие можно считать стат.значимым.

Это значит, что скорее всего топ-4 сегмента тратят за покупку в среднем одинаково, но в сегментации отличаются по другим параметрам - Кол-ве покупок и в сроке между покупками.

Результат исследования¶

Общий вывод исследования

  1. В нашем распоряжении оказался файл ecom_dataset_upd с описанием транзакций интернет-магазина товаров для дома и быта «Пока все ещё тут». Данные для анализа представлены с 1 октября 2018 года по 31 января 2020 года.
  2. Перед проведением анализа мы провели предобработку данных:
    • Названия колонок приведены к нижнему регистру;
    • Датасеты проверены на отсутствие явных и неявных дубликатов, пропусков в данных;
    • Тип данных некоторых колонок приведен к нужному формату;
    • Добавили несколько колонок для удобства дальнейшего анализа;
  3. В рамках исследовательского анализа были выявлены следующие особенности:

    • В нашем распоряжении оказались данные о 3432 заказах и 2362 уникальных пользователей. Период исследования - 1 октября 2018 - 31 января 2020 года.
    • Мы определили 2 разных периода оптимальных для анализа:
      • полный 2019 год для анализа финансовых показателей;
      • весь датасет для анализа поведения пользователей;
    • При общей выручке в 2,5 млн. рублей за 2019 год, месячная выручка лишь 2 месяца достигала показатель 300 тыс. - в феврале и апреле. Самые активные продажи произошли в начале года. Общая тенденция - уменьшение объемов продаж.
    • Средний чек покупателя в 2019 году составил 1020.5 руб. Общий тренд - снижение среднего чека в течение года.
    • Мы провели ABC-анализ, который показал, что 27% товарного ассортимента магазина приносят компании 80% всей выручки (Категория А). Такие товары выгоднее всего продавать и на их реализации можно будет в будущем сконцентрироваться. Категория С (43% товаров) принесла компании всего 5% всей выручки. Мы сохранили категории товаров в отдельные списки, которые в дальнейшем можно будет передать маркетинговому отделу.
    • Стандартный покупатель интернет-магазина в среднем делает 1-2 покупки.
    • В среднем между повторными покупками покупателей проходит 221 день. Медианное число - 262 дня. Это значит, что наш "типичный" покупатель, если и возвращается за повторной покупкой, то минимум через полгода, а то и дольше.
  4. Для формирования рекомендаций для компании иы провели RFM-анализ покупателей интернет-магазина. Благодаря анализу мы смогли сегментировать 7 групп пользователей, к каждой из которых можно применять персонализированный подход в продажах.

  5. Разбивка по сегментам демонстрирует, что практически половину всей выручки компании обеспечивает сегмент "Не можем их потерять" - 1,6 млн рублей. Меньше всего прибыли приносит сегмент "Необходима активация" - 26 тыс. рублей.
  6. Дополнительно выделили товарные категории ассортимента интернет-магазина - всего 8 категорий. Больше всего выручки приносит категория Хранение - более 1,1 млн. рублей. И именно в этой категории больше всего прибыль от товаров группы А. Меньше всего прибыли поступает от категории Декор - не более 100 тыс. рублей. Возможно стоит увеличить товарный ассортимент этой категории.
  7. Проанализировали поведение пользователей каждого сегмента - какие товары пользователи покупают больше всего.
    • Сегмент "Не можем их потерять" предпочитают покупать товары категории Хранение, Товары для дома и Товары для ванной. Меньше всего покупают Декор - не более 22 тыс. рублей.
    • Сегмент "Чемпионы" также предпочитает товары категории Хранение, однако остальные категории распределены более равномерно - практически равные доли у Товаров для Дома и Ванной (103 и 100 тыс.). Растения и Рассада, Текстиль и Другое - порядка 50 тыс. рублей на каждую категорию.
    • Более однородный запрос у сегмента "Перспективные". Здесь категории распределились практически равномерно. В порядке убывания - Товары для дома, Хранение, Товары для ванной (все по 40-48 тыс.) и др.
  8. Проанализировали динамику продаж товаров разных категорий. Выявили яркую сезонность у категории Растения и рассада - пик продаж приходится на март - июнь.
  9. Проверили гипотезы о стат.значимости различий между сегментами покупателей по медианному чеку и медианной выручке с покупателя. В случае с проверкой средней выручки не удалось опровергнуть нулевую гипотезу в случае с сегментами Чемпионы, Лояльные и Потенциальные. Это значит, что мы всё ещё можем использовать нашу сегментацию в рамках подготовки рекомендаций для бизнеса, но с пометкой, что сегменты Чемпионы, Лояльные и Потенциальные возможно переформатировать на более укрупненные. Средний чек не имеет стат.значимое отличие в случае сегментов Не можем их потерять, Чемпионы, Лояльные и Потенциальные. Скорее всего топ-4 сегментов тратят за покупку в среднем одинаково, но в сегментации отличаются по другим параметрам - Кол-ве покупок и в сроке между покупками.

Общие рекомендации для интернет-магазина

  1. обратить внимание на снижающиеся объемы продаж, а также на снижение среднего чека в течение всего года.
  2. Дополнительно проработать стратегию по удержанию пользователей. Проанализировать возможности проведения акций, которые бы участили визиты пользователей на сайт.
  3. Проанализировать списки товаров, которые продаются меньше всего (Категория С). Принять решение о выводе из реализации, либо увеличить промотирование этих товаров.
  4. Проработать персонализированный подход к клиентам, основываясь на сегментации и предпочтениях каждого сегмента.

    • Сегмент "Не можем их потерять" - стратегически важные клиенты. Стратегия: можем направить им особые предложения, участие в программе лояльности. Предлагать категории Хранение, Товары для дома и Товары для ванной.

    • Сегмент "Чемпионы" - регулярные покупатели. Стратегия: подготовить программу лояльности, начислить бонусы, предложить подарок за покупку или достижение конкретного уровня чека. Предлагать - Товаров для Дома и Ванной.

    • Сегмент "Лояльные" - взаимодействуют с компанией нерегулярно, но при этом тратят большие суммы. Стратегия: необходимо изучить потребности клиентов и направить персонализированные предложения. Таким клиентам не рекомендуется предлагать скидки. Категории -
    • Сегмент "Потенциальные" - такие покупатели помнят о компании, когда-то совершали покупки, но сейчас перестали это делать. Стратегия: необходимо напомнить о компании посредством рассылки, разработки стимулирующих акций, начисления бонусов на карту лояльности. Категории - Товары для ванной, Растения и Рассада.
    • Сегмент "Перспективные" - при правильном взаимодействии могут стать постоянными клиентами. Стратегия: Этому сегменту нужно оказывать активную помощь в выборе товара, давать полезную информацию. Категории - Товары для дома, ванной и кухни.
    • Сегмент "Необходимо внимание" - необходимо получить обратную связь, чтобы понять причину отказа от приобретения товаров. Можно начислить бонусы и предложить выгодную акцию. Таких покупателей можно вернуть и сделать постоянными клиентами.
    • Сегмент "Необходима активация" - клиенты давно не взаимодействуют с компанией. Стратегия: Можно восстановить контакт с помощью рассылки. Настаивать и тратить много времени/сил не рекомендуется.